package plugin

import (
	"context"
	"fmt"
	v1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/runtime"
	utilfeature "k8s.io/apiserver/pkg/util/feature"
	"k8s.io/klog/v2"
	v1helper "k8s.io/kubernetes/pkg/apis/core/v1/helper"
	"k8s.io/kubernetes/pkg/features"
	pluginhelper "k8s.io/kubernetes/pkg/scheduler/framework/plugins/helper"
	framework "k8s.io/kubernetes/pkg/scheduler/framework/v1alpha1"
	schedutil "k8s.io/kubernetes/pkg/scheduler/util"
	"math"
)

const (
	Name = "TddhPlugin"
)

func (t *TddhScheduler) Name() string {
	return Name
}

func (t *TddhScheduler) Filter(ctx context.Context, _ *framework.CycleState, pod *v1.Pod, nodeInfo *framework.NodeInfo) *framework.Status {
	/*	if nodeInfo.Node().Labels["scheduler"] != "true" {
		return framework.NewStatus(framework.Unschedulable, "Node: "+nodeInfo.Node().Name)
	}*/
	return nil
}

func (t *TddhScheduler) Score(ctx context.Context, state *framework.CycleState, pod *v1.Pod, nodeName string) (int64, *framework.Status) {
	klog.Info("Score Function ", pod.Name, " ", nodeName)
	klog.Flush()
	nodeInfo, err := t.handle.SnapshotSharedLister().NodeInfos().Get(nodeName)
	if err != nil {
		return 0, framework.NewStatus(framework.Error, fmt.Sprintf("getting node %q from Snapshot: %v", nodeName, err))
	}

	//clientSet.RESTClient().Get().RequestURI("https://kubernetes/apis/metrics.k8s.io/v1beta1/nodes").DoRaw(context.Background())

	return t.score(pod, nodeInfo)
}

func (r *resourceAllocationScorer) score(pod *v1.Pod, nodeInfo *framework.NodeInfo) (int64, *framework.Status) {
	node := nodeInfo.Node()

	if node == nil {
		return 0, framework.NewStatus(framework.Error, "node not found")
	}
	if r.resourceToWeightMap == nil {
		return 0, framework.NewStatus(framework.Error, "resources not found")
	}

	requested := make(resourceToValueMap, len(r.resourceToWeightMap))
	allocatable := make(resourceToValueMap, len(r.resourceToWeightMap))
	for resource := range r.resourceToWeightMap {
		allocatable[resource], requested[resource] = calculateResourceAllocatableRequest(nodeInfo, pod, resource)
	}
	var score int64

	score = r.scorer(requested, allocatable)
	klog.Info(node.Name, " score ", score)
	klog.Flush()

	return score, nil
}

func balancedResourceScorer(requested, allocatable resourceToValueMap) int64 {
	cpuFraction := fractionOfCapacity(requested[v1.ResourceCPU], allocatable[v1.ResourceCPU])          //0.01366867140513942
	memoryFraction := fractionOfCapacity(requested[v1.ResourceMemory], allocatable[v1.ResourceMemory]) //0.04609479379996608
	defer klog.Flush()
	klog.Info("balancedResourceScorer cpu ", cpuFraction, " mem ", memoryFraction)
	if cpuFraction >= 1 || memoryFraction >= 1 {

		return 0
	}

	diff := math.Abs(cpuFraction - memoryFraction) //0.032426   0.967574
	klog.Info("balancedResourceScorer diff ", diff)
	return int64((1 - diff) * float64(framework.MaxNodeScore))
}

func fractionOfCapacity(requested, capacity int64) float64 {
	if capacity == 0 {
		return 1
	}
	return float64(requested) / float64(capacity)
}

func calculateResourceAllocatableRequest(nodeInfo *framework.NodeInfo, pod *v1.Pod, resource v1.ResourceName) (int64, int64) {
	defer klog.Flush()
	podRequest := calculatePodResourceRequest(pod, resource)
	//realResourceRate := getRealResourcesLoad(clientset)
	switch resource {
	case v1.ResourceCPU:
		realCpu := RealResourceRate[nodeInfo.Node().Name][v1.ResourceCPU] / 1000000
		nonZeroRequested := 1 - float64(nodeInfo.NonZeroRequested.MilliCPU)/float64(nodeInfo.Node().Status.Capacity.Cpu().MilliValue())
		klog.Info(nodeInfo.Node().Name, " nonZeroRequested Cpu ", nonZeroRequested, " : ", nodeInfo.NonZeroRequested.MilliCPU, "/", nodeInfo.Node().Status.Capacity.Cpu().MilliValue())

		allocatable := float64(nodeInfo.Node().Status.Capacity.Cpu().MilliValue()-realCpu) * (nonZeroRequested)
		klog.Info(nodeInfo.Node().Name, " Allocatable Cpu ", int64(allocatable), " request ", podRequest)
		return int64(allocatable), podRequest

	case v1.ResourceMemory:
		realMem := RealResourceRate[nodeInfo.Node().Name][v1.ResourceMemory] * 1024
		nonZeroRequested := 1 - float64(nodeInfo.NonZeroRequested.Memory)/float64(nodeInfo.Node().Status.Capacity.Memory().MilliValue())
		klog.Info(nodeInfo.Node().Name, " nonZeroRequested Memory", nonZeroRequested, " : ", nodeInfo.NonZeroRequested.Memory, "/", nodeInfo.Node().Status.Capacity.Memory().MilliValue())

		allocatable := float64(nodeInfo.Node().Status.Capacity.Memory().Value()-realMem) * (nonZeroRequested)
		klog.Info("calculateResource Memory ", nodeInfo.Node().Name, " Allocatable Memory ", int64(allocatable), " request ", podRequest)

		return int64(allocatable), podRequest

	default:
		if v1helper.IsScalarResourceName(resource) {
			return nodeInfo.Allocatable.ScalarResources[resource], podRequest
		}
	}

	return 0, 0
}

func calculatePodResourceRequest(pod *v1.Pod, resource v1.ResourceName) int64 {
	var podRequest int64
	for i := range pod.Spec.Containers {
		container := &pod.Spec.Containers[i]
		value := schedutil.GetNonzeroRequestForResource(resource, &container.Resources.Requests)
		podRequest += value
	}

	for i := range pod.Spec.InitContainers {
		initContainer := &pod.Spec.InitContainers[i]
		value := schedutil.GetNonzeroRequestForResource(resource, &initContainer.Resources.Requests)
		if podRequest < value {
			podRequest = value
		}
	}

	if pod.Spec.Overhead != nil && utilfeature.DefaultFeatureGate.Enabled(features.PodOverhead) {
		if quantity, found := pod.Spec.Overhead[resource]; found {
			podRequest += quantity.Value()
		}
	}

	return podRequest
}

func (t *TddhScheduler) ScoreExtensions() framework.ScoreExtensions {
	return t
}

func (t *TddhScheduler) NormalizeScore(ctx context.Context, state *framework.CycleState, pod *v1.Pod, scores framework.NodeScoreList) *framework.Status {
	klog.Info("NormalizeScore ", pod.Name)
	klog.Flush()
	return pluginhelper.DefaultNormalizeScore(framework.MaxNodeScore, false, scores)
}

func (t *TddhScheduler) PreBind(ctx context.Context, state *framework.CycleState, pod *v1.Pod, nodeName string) *framework.Status {
	if nodeInfo, err := t.handle.SnapshotSharedLister().NodeInfos().Get(nodeName); err != nil {
		return framework.NewStatus(framework.Error, fmt.Sprintf("prebind get node info error: %v", nodeName))
	} else {
		klog.Info(pod.Name, " bind ", nodeInfo.Node().Name)
		klog.Flush()
		return framework.NewStatus(framework.Success, "")
	}

}

func New(_ runtime.Object, f framework.FrameworkHandle) (framework.Plugin, error) {
	return &TddhScheduler{
		handle: f,
		resourceAllocationScorer: resourceAllocationScorer{
			Name,
			balancedResourceScorer,
			defaultRequestedRatioResources,
		},
	}, nil

}
